/*
Copyright 2011 Raymond Giorgi

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

   http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
 */

package edu.pitt.cs3530.applicationcontrolling;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import edu.pitt.cs3530.SystemInfo;
import edu.pitt.cs3530.configuration.FrontEndConfiguration;
import edu.pitt.cs3530.identifier.HandshakeInfo;
import edu.pitt.cs3530.shareddatastructures.Utilization;

public class ServerController {

	public enum Scale {
		ON, OFF, UP, DOWN, NONE
	};

	private static ServerController instance;

	public static ServerController getInstance() {
		if (instance == null) {
			instance = new ServerController();
		}
		return instance;
	}

	private Map<String, ServerInfo> map = null;
	private ServerInfo alwaysOn = null;
	private boolean initialized = false;
	private ServerInfo database = null;

	private class ServerInfo {
		private String hostname;
		private String macAddress;
		private Machine machine;
		private boolean isRunning;

		private ServerInfo(String hostname, String macAddress,
				Machine frequencies) {
			this.hostname = hostname;
			this.macAddress = macAddress;
			this.machine = frequencies;
			isRunning = true;
		}
	}

	private ServerController() {
		map = new HashMap<String, ServerInfo>();
	}

	public synchronized void createServer(HandshakeInfo info) {
		if (!info.isDatabase()) {
			ServerInfo sInfo = getServerInfo(info.getHostname());
			System.out.println(sInfo);
			System.out.println(info.getHostname());
			// Assume we're waking back up
			if (sInfo != null) {
				sInfo = new ServerInfo(info.getHostname(),
						info.getMacAddress(), info.getFreqs());
				sInfo.isRunning = true;
				map.put(info.getHostname(), sInfo);
				// try {
				// sInfo.machine.scaleToMin(sInfo.hostname, false);
				// } catch (IOException e) {
				// e.printStackTrace();
				// }
				// Assume this is the first time we're registering
			} else {
				sInfo = new ServerInfo(info.getHostname(),
						info.getMacAddress(), info.getFreqs());
				System.out.println("toShutdown");
				System.out.println(map.size());
				if (map.size() > 0) {
					try {
						System.out.println("map size:" + map.size());
						System.out.println("shutdown");
						map.put(info.getHostname(), sInfo);
						this.shutDown(sInfo.hostname);
					} catch (IOException e) {
						e.printStackTrace();
					}
				} else {
					map.put(info.getHostname(), sInfo);
					this.alwaysOn = sInfo;
					try {
						sInfo.machine.scaleToMin(info.getHostname(), false);
					} catch (IOException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
				}
			}
		} else {
			this.database = new ServerInfo(info.getHostname(), info
					.getMacAddress(), info.getFreqs());
		}
	}

	public boolean scaleUpDatabase() {
		if (database != null && database.machine.canScaleUp()) {
			try {
				database.machine.scaleUpSmallest(database.hostname);
				return true;
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
		return false;
	}

	public String getDatabaseHostname() {
		if (database != null) {
			return database.hostname;
		}
		return null;
	}

	public boolean scaleDownDatabase() {
		if (database != null && database.machine.canScaleDown()) {
			try {
				database.machine.scaleLargestDeltaDown(database.hostname);
				return true;
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
		return false;
	}

	/**
	 * Returns true if at least one machinene is running and registered. NOTE:
	 * The only time this should return false is when a machinene hasn't started
	 * yet
	 * 
	 * @return true if at least one machinene is running
	 */
	public boolean oneIsRunning() {
		for (ServerInfo info : map.values()) {
			if (info.isRunning) {
				System.out.println("running: " + info.hostname);
				return true;
			}
		}
		return false;
	}

	public ServerInfo getServerInfo(String hostname) {
		return map.get(hostname);
	}

	public boolean moreThanOneRunning() {
		int count = 0;
		for (ServerInfo info : map.values()) {
			if (info.isRunning) {
				count++;
			}
		}
		return count > 1;
	}

	/**
	 * Returns true if the name of the host is in our array
	 * 
	 * @param hostname
	 * @return true if the name of the host is in our array
	 */
	public boolean hostExists(String hostname) {
		return map.containsKey(hostname);
	}

	public Scale scaleUpToFrequency(double frequency, boolean onOff)
			throws IOException {
		Scale scale = Scale.NONE;
		boolean scaled = false;
		System.out.println("up.pre: " + this.getTotalFreq() + "\t" + frequency);
		double absoluteStartFrequency = this.getTotalFreq();
		while (this.getTotalFreq() < frequency) {
			double startFrequency = this.getTotalFreq();
			ServerInfo info = findMinDeltaUp();
			if (info != null) {
				info.machine.scaleUpSmallest(info.hostname);
				scaled = true;
				scale = Scale.UP;
			}
			if (startFrequency == this.getTotalFreq()) {
				break;
			}
		}
		if (onOff && this.getTotalFreq() < frequency && !scaled) {
			List<ServerInfo> shutdown = this.getAllShutDown();
			if (!shutdown.isEmpty()) {
				for (ServerInfo info : shutdown) {
					if (info != this.alwaysOn) {
						this.wakeUpServer(info.hostname);
						scale = Scale.ON;
						break;
					}
				}
			}
		}
		if (scale == Scale.ON) {
			// scaleDownToFrequency(frequency, false);
		} else if (this.getTotalFreq() == absoluteStartFrequency) {
		} else {
			scale = Scale.UP;
		}
		System.out
				.println("up.post: " + this.getTotalFreq() + "\t" + frequency);
		return scale;
	}

	private ServerInfo findMinDeltaUp() {
		ServerInfo min = null;
		double minRatio = Double.MAX_VALUE;
		for (ServerInfo info : getAllRunning()) {
			if (info.machine.canScaleUp()) {
				double ratio = info.machine.getSmallestDeltaUp();
				if (ratio < minRatio) {
					min = info;
					minRatio = ratio;
				}
			}
		}
		return min;
	}

	public Scale scaleDownToFrequency(double frequency, boolean onOff)
			throws IOException {
		Scale scale = Scale.NONE;
		if (onOff) {
			ServerInfo info = this.canShutDown(frequency);
			if (info != null) {
				this.shutDown(info.hostname);
				info = this.canShutDown(frequency);
				scaleUpToFrequency(frequency, false);
				scale = Scale.OFF;
			}
		}
		double absoluteStartFrequency = this.getTotalFreq();
		System.out.println("down.pre: " + absoluteStartFrequency + "\t"
				+ frequency);
		// if (scale == Scale.NONE) {
		boolean canScaleDownMore = true;
		double totalFrequency = this.getTotalFreq();
		while (canScaleDownMore) {
			ServerInfo info = this.findMaxDeltaDown();
			if (info != null) {
				double frequencyChange = info.machine
						.getFrequencyDifferenceOfLargestDelta();
				if (totalFrequency - frequencyChange > frequency) {
					totalFrequency -= frequencyChange;
					info.machine.scaleLargestDeltaDown(info.hostname);
					scale = Scale.DOWN;
				} else {
					info = null;
				}
			}
			if (info == null) {
				info = this.findSmallestFrequencyDown();
				if (info != null) {
					double frequencyChange = info.machine
							.getSmallestFrequencyDown();
					if (totalFrequency - frequencyChange > frequency) {
						totalFrequency -= frequencyChange;
						info.machine.scaleSmallestFrequencyDown(info.hostname);
						scale = Scale.DOWN;
					} else {
						info = null;
					}
				}
			}
			if (info == null) {
				canScaleDownMore = false;
			}
		}
		// }
		System.out.println("down.post: " + this.getTotalFreq() + "\t"
				+ frequency);
		return scale;
	}

	public Scale scaleDownOne(boolean onOff) throws IOException {
		Scale scale = Scale.NONE;
		double scaledDownFreq = 0;
		if (onOff) {
			double totalLost = 0.0;
			List<ServerInfo> allRunning = getAllRunning();
			if (allRunning.size() > 1) {
				for (ServerInfo info : getAllRunning()) {
					if (!info.machine.canScaleDown() && info != this.alwaysOn) {
						scaledDownFreq = info.machine.getTotalFrequency();
						this.shutDown(info.hostname);
						totalLost = info.machine.getTotalFrequency();
						scale = Scale.OFF;
						this.scaleUpToFrequency(this.getTotalFreq() + .75
								* totalLost, false);
					}
				}
			}
		}
		if (scale == Scale.NONE) {
			ServerInfo info = findMaxDeltaDown();
			if (info != null) {
				info.machine.scaleLargestDeltaDown(info.hostname);
				scale = Scale.DOWN;
			}
		}
		if (scale == Scale.OFF) {
			this
					.scaleUpToFrequency(this.getTotalFreq() + scaledDownFreq,
							false);
		}
		return scale;
	}

	private ServerInfo findMaxDeltaDown() {
		ServerInfo max = null;
		double maxRatio = -Double.MAX_VALUE;
		for (ServerInfo info : getAllRunning()) {
			if (info.machine.canScaleDown()) {
				double ratio = info.machine.getLargestDeltaDown();
				if (ratio > maxRatio) {
					max = info;
					maxRatio = ratio;
				}
			}
		}
		return max;
	}

	private ServerInfo findSmallestFrequencyDown() {
		ServerInfo min = null;
		double minRatio = Double.MAX_VALUE;
		for (ServerInfo info : getAllRunning()) {
			if (info.machine.canScaleDown()) {
				double frequency = info.machine.getSmallestFrequencyDown();
				if (frequency < minRatio) {
					min = info;
					minRatio = frequency;
				}
			}
		}
		return min;
	}

	private ServerInfo canShutDown(double targetFrequency) {
		ServerInfo retval = null;
		List<ServerInfo> list = new ArrayList<ServerInfo>(this.getAllRunning());
		if (list.size() > 1) {
			ServerInfo toShutDown = null;
			if (list.get(0) == this.alwaysOn) {
				toShutDown = list.remove(1);
			} else {
				toShutDown = list.remove(0);
			}
			double totalFrequencies = 0.0;
			for (ServerInfo info : list) {
				totalFrequencies += info.machine.getMaxTotalFrequency();
			}
			if (totalFrequencies > targetFrequency) {
				retval = toShutDown;
			}
			System.out.println(totalFrequencies + "!!!!!" + targetFrequency);
		}
		return retval;
	}

	public List<ServerInfo> getAllShutDown() {
		List<ServerInfo> infos = new ArrayList<ServerInfo>();
		for (ServerInfo info : map.values()) {
			if (!info.isRunning) {
				infos.add(info);
			}
		}
		return infos;
	}

	public void shutDown(String hostname) throws IOException {
		ServerInfo info = null;
		for (ServerInfo serverInfo : map.values()) {
			if (hostname.equals(serverInfo.hostname)) {
				info = serverInfo;
			}
		}
		System.out.println(info);
		if (info != null) {
			info.isRunning = false;
			info.machine.shutDown(info.hostname);
			Utilization.clearUtilization(info.hostname);
		}
	}

	public List<ServerInfo> getAllRunning() {
		List<ServerInfo> infos = new ArrayList<ServerInfo>();
		for (ServerInfo info : map.values()) {
			if (info.isRunning) {
				infos.add(info);
			}
		}
		return infos;
	}

	/**
	 * Returns true if at least one machinene can scale down
	 * 
	 * @param onOff
	 * @return true if at least one machinene can scale down
	 */
	public boolean oneCanScaleDown(boolean onOff) {
		synchronized (map) {
			for (ServerInfo info : map.values()) {
				if (info.isRunning && info.machine.canScaleDown()) {
					return true;
				}
			}
			if (onOff) {
				for (ServerInfo info : map.values()) {
					if (info.isRunning && info != this.alwaysOn) {
						return true;
					}
				}
			}
		}
		return false;
	}

	public double getTotalFreq() {
		double total = 0.0;
		for (ServerInfo info : map.values()) {
			if (info.isRunning) {
				total += info.machine.getTotalFrequency();
			}
		}
		return total;
	}

	public void shutDownOne(String hostname) throws IOException {
		if (!oneIsRunning()) {
			ServerInfo info = map.get(hostname);
			if (info.isRunning) {
				info.machine.shutDown(hostname);
			}
		}
	}

	/**
	 * Turn on a named server
	 * 
	 * @param hostname
	 *            The server to turn on
	 */
	public void wakeUpServer(String hostname) {
		ServerInfo info = map.get(hostname);
		if (info != null) {
			try {
				SystemInfo.executeCommand(FrontEndConfiguration.getInstance()
						.getWakePrefix()
						+ hostname);
				SystemInfo.executeCommand(FrontEndConfiguration.getInstance()
						.getWakePrefix()
						+ hostname);
				SystemInfo.executeCommand(FrontEndConfiguration.getInstance()
						.getWakePrefix()
						+ hostname);
				info.isRunning = true;
				info.machine.scaleToMin(info.hostname, true);
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}

	public ServerInfo getAlwaysOn() {
		return alwaysOn;
	}

	public List<ServerInfo> getAllAtMin() {
		List<ServerInfo> list = new ArrayList<ServerInfo>();
		for (ServerInfo info : map.values()) {
			if (!info.machine.canScaleDown()) {
				list.add(info);
			}
		}
		return list;
	}
}
